package otso.se.network.sockets.communication.abstraction.tcp;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.atomic.AtomicBoolean;

import org.restlet.data.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import otso.se.network.sockets.communication.abstraction.HttpRequest;
import otso.se.network.sockets.communication.abstraction.HttpResponse;

/*
 * WARNING: When to create and close an input or output stream does matter!
 * 			Be cautious whenever you have temptations of changing them!
 * 
 * (a good similar example: http://www.daniweb.com/software-development/java/threads/259042)
 */
public class TCPServer {
	final int port;

	final Logger logger = LoggerFactory.getLogger(TCPServer.class);
	final AtomicBoolean exit = new AtomicBoolean(false);

	ServerSocket serverSocket = null;
	IUpcomingHttpConnectionListener listener;
	
	public TCPServer(int port) {
		this.port = port;
	}

	public void start() {
		logger.debug("Running Server...");

		try {
			serverSocket = new ServerSocket(this.port);
			exit.set(false);
			acceptConnectionObject();
		} catch (IOException e) {
			System.err.println("Could not listen on port: " + this.port);
			System.exit(1);
		}
	}

	public void stop() {
		logger.debug("Stopping Server...");
		exit.set(true);
		try {
			serverSocket.close();
			logger.debug("Server socket closed.");
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}

	private void acceptConnectionObject() {
		Socket clientSocket = null;
		try {
			while (!exit.get()) {
				clientSocket = serverSocket.accept();
				logger.debug("Connection accepted.");
				
				treatClientConnection(clientSocket);

				clientSocket.close();
				logger.debug("Client socket closed.");
			}
		} catch (SocketException se) {
			logger.error("TCPServer closed.");
		} catch (IOException ie) {
			logger.error("Accept failed.");
			StackTraceElement[] stes = ie.getStackTrace();
			for (StackTraceElement ste : stes) {
				logger.error(ste.getMethodName() + ":" + ste.getLineNumber());
			}
			System.exit(1);
		}
	}
	
	private void treatClientConnection(Socket clientSocket) {
		ObjectInputStream in = null;
		ObjectOutputStream out = null;
		try {
			in = new ObjectInputStream(clientSocket.getInputStream());
			logger.debug("Awaiting data.");
			
			// TODO for now on, I'd be better to do it in a new thread
			final Object rcvd;
			HttpResponse response = null;
			try {
				rcvd = in.readObject();
				logger.debug("Received message: " + rcvd.toString());
				
				if( rcvd instanceof HttpRequest ) {
					if( this.listener!=null ) {
						response = this.listener.handle((HttpRequest)rcvd);
					} else {
						response = new HttpResponse(Status.SERVER_ERROR_SERVICE_UNAVAILABLE, System.currentTimeMillis());
						response.setData("Http connection listener not registered yet.");
					}
				} else {
					response = new HttpResponse(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, System.currentTimeMillis());
					response.setData("The request received in the socket was not a HttpResquest.");			
				}
			} catch (ClassNotFoundException cnfe) {
				response = new HttpResponse(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, System.currentTimeMillis());
				response.setData("The request received in the socket belongs to an unknown class.");
			} finally {
				// response shouldn't be null!
				out = sendResponse(clientSocket, response);
			}
		} catch (IOException ie) {
				logger.error("Error reading the object from the socket.");
		} finally{
			try {
				if (in!=null) in.close();
				if (out!=null) out.close();
				if (!clientSocket.isClosed()){
					clientSocket.close();
					logger.debug("Client socket closed.");
				}
			} catch (IOException e) {
				logger.error("Client could not be properly closed.");
			}
			
		}
	}
	
	/**
	 * @return
	 * 		The output stream that must be closed (it is not closed here cause
	 *  problems with the order of closing were found in some tests).
	 */
	private ObjectOutputStream sendResponse(Socket clientSocket, HttpResponse response) {
		try {
			final ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream());
			out.writeObject(response);
			out.flush();
			logger.debug("Response sent.");
			return out;
		} catch (IOException e) {
			logger.error("Error writing the Http response.");
		}
		return null;
	}

	public void addConnectionListener(IUpcomingHttpConnectionListener listener) {
		this.listener = listener;
	}
}